# -*- coding: utf-8 -*-
#
# Copyright (c) 2012, Rafael Pivato
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#     * Redistributions of source code must retain the above copyright
#       notice, this list of conditions and the following disclaimer.
#     * Redistributions in binary form must reproduce the above copyright
#       notice, this list of conditions and the following disclaimer in the
#       documentation and/or other materials provided with the distribution.
#     * Neither the name of Rafael Pivato nor the names of its contributors
#       may be used to endorse or promote products derived from this
#       software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL RAFAEL PIVATO BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import random

from datetime import datetime, timedelta
from google.appengine.ext import db
from wowgae.model.account import Account


class Character(db.Model):
    """Represents a character in our database."""

    account = db.StringProperty(required=False, default='')
    achievement_points = db.IntegerProperty(required=True, default=0)
    klass = db.IntegerProperty(required=True)
    gender = db.IntegerProperty(required=True)
    last_modified = db.IntegerProperty(required=True, default=0)
    level = db.IntegerProperty(required=True)
    locked = db.BooleanProperty(default=False)
    member_until = db.IntegerProperty(required=True)
    name = db.StringProperty(required=True)
    race = db.IntegerProperty(required=True)
    rank = db.IntegerProperty(required=True)
    renamed = db.BooleanProperty(required=True, default=False)
    thumbnail = db.StringProperty(required=True, indexed=False)

    def detect_rename(self, character):
        """Detects a different character under the same name.

        This is useful to detect we are talking about a different character
        that possibly paid for a character transfer or name change.

        """
        if self.klass != character.klass or self.level > character.level:
            return True
        return False

    @classmethod
    def get_by_name(cls, name):
        """Return the character with the given name."""
        name = name.lower()
        return Character.get_by_key_name(name)

    @classmethod
    def get_member_by_name(cls, name):
        """Returns the given character that is member of our guild."""
        name = name.lower()
        # TODO: called too many times on a guild update
        members_since = Guild.members_since()
        query = cls.gql("WHERE __key__ = KEY('Character', :1) AND " +
                        "member_until >= :2", name, members_since)
        return query.get()

    @classmethod
    def get_all_for_account(cls, account):
        """Returns all characters activated for one site account."""
        user_id = account
        if isinstance(account, Account):
            user_id = account.key().id_or_name()
        query = cls.all()
        query.filter('account =', user_id)
        accounts = query.fetch(100)
        accounts = sorted(accounts, key=lambda i: i.name)
        return accounts

    @classmethod
    def update_member(cls, member, ranks={'admin': 0, 'officer': 0}):
        """Updates character's guild membership."""
        character = member.character
        key_name = character.name.lower()

        def txn():

            # We don't need to update membership history by now
            insert_new_membership = False

            # Tries to fetch existing information for our character
            model = cls.get_by_key_name(key_name)
            if model is None:

                # Create new character and store it our data store
                properties = {
                    'achievement_points': character.achievement_points,
                    'klass': character.klass,
                    'gender': character.gender,
                    'last_modified': 0,
                    'level': character.level,
                    'name': character.name,
                    'race': character.race,
                    'thumbnail': character.thumbnail,
                    'member_until': character.guild.last_modified,
                    'rank': member.rank,
                }
                model = cls(key_name=key_name, **properties)
                model.put()
                insert_new_membership = 'new'

            elif model.member_until < character.guild.last_modified or True:

                # We will only compute existing members if our data is fresh

                if model.detect_rename(character):

                    # Blizzard paid services can bring a problem to us
                    model.member_until = character.guild.last_modified
                    model.renamed = True
                    model.put()
                    insert_new_membership = 'rename'

                else:

                    # Existing model will be updated with fresh data
                    model.achievement_points = character.achievement_points
                    model.gender = character.gender
                    model.level = character.level
                    model.race = character.race
                    model.thumbnail = character.thumbnail
                    model.member_until = character.guild.last_modified
                    model.rank = member.rank
                    model.put()

                    # Update account membership data
                    if model.account:
                        for some_role in ['admin', 'officer']:
                            if model.rank <= ranks[some_role]:
                                account_role = some_role
                                break
                        else:
                            account_role = 'member'
                        Account.update_membership(model.account, model.name,
                            character.guild.last_modified, account_role)

                    # Need to check about membership history
                    query = CharacterMembership.all()
                    query.ancestor(model)
                    query.order('-timestamp')
                    membership = query.get()
                    if membership is None:
                        insert_new_membership = 'adjust'
                    elif membership.timestamp >= Guild.members_since():
                        membership.timestamp = character.guild.last_modified
                        membership.to_date = datetime.utcnow()
                        membership.put()
                    else:
                        insert_new_membership = 'back'

            # Insert membership history information if requested
            if insert_new_membership:
                utc_now = datetime.utcnow()
                new_properties = {
                    'from_date': utc_now,
                    'to_date': utc_now,
                    'timestamp': character.guild.last_modified,
                    'reason': insert_new_membership,
                }
                new_membership = CharacterMembership(parent=model,
                                                     **new_properties)
                new_membership.put()

        xg_on = db.create_transaction_options(xg=True)
        db.run_in_transaction_options(xg_on, txn)

    @classmethod
    def missing_activation(cls):
        query = cls.all()
        query.filter('account =', '')
        return query.fetch(50)

    @classmethod
    def run_low_level_missing_activation(cls):
        character_query = cls.all()
        character_query.order('level')
        for character in character_query.run():
            if character.account == '' and \
            character.member_until >= Guild.members_since():
                activation_query = CharacterActivation.all()
                activation_query.ancestor(character)
                if activation_query.count(1) == 0:
                    yield character


class CharacterMembership(db.Model):
    """Represents the membership condition of a character in a given time."""

    from_date = db.DateTimeProperty(required=True)
    to_date = db.DateTimeProperty(required=True)
    timestamp = db.IntegerProperty(required=True)
    reason = db.StringProperty(choices=['new', 'back', 'rename', 'adjust'])
    note = db.TextProperty(default='')


class CharacterActivation(db.Model):
    """Controls activation of specific characters."""

    email = db.EmailProperty(required=True)
    golds = db.IntegerProperty(required=True, indexed=False)
    silvers = db.IntegerProperty(required=True, indexed=False)
    coppers = db.IntegerProperty(required=True, indexed=False)
    created = db.DateTimeProperty(required=True)

    def amount(self):
        coppers = self.coppers
        silvers = self.silvers * 100
        golds = self.golds * 10000
        return coppers + silvers + golds

    def confirm(self, ranks={'admin': 0, 'officer': 0}):
        """Confirms an activation code and binds a character to a member."""

        def txn():
            name = self.key().parent().name().capitalize()
            user_id = self.key().id_or_name()
            character = self.parent()
            character.account = user_id
            account = Account.get_by_user_id(user_id)
            if account is None:
                for some_role in ['admin', 'officer']:
                    if character.rank <= ranks[some_role]:
                        role = some_role
                        break
                else:
                    role = 'member'
                Account.create_for_user_id(user_id, name=name,
                                           email=self.email,
                                           role=role,
                                           locked=character.locked)
            elif account.locked:
                character.locked = True
            elif character.locked and not account.locked:
                Account.update_for_user_id(user_id, locked=True)
            character.put()
            self.delete()
        xg_on = db.create_transaction_options(xg=True)
        db.run_in_transaction_options(xg_on, txn)

    def name(self):
        """Name for the character related to this activation."""
        return self.key().parent().name().capitalize()

    def character(self):
        """Returns the character related to this activation."""
        return self.parent()

    @classmethod
    def start(cls, character, user):
        """Creates an activation code for the given character."""
        parent = None
        if isinstance(character, Character):
            parent = character.key()
        else:
            parent = db.Key.from_path('Character', character.lower())
        init_data = {
            'parent': parent,
            'key_name': user.user_id(),
            'email': user.email(),
            'golds': random.randint(1000, 9999),
            'silvers': random.randint(10, 99),
            'coppers': random.randint(10, 99),
            'created': datetime.utcnow(),
        }
        activation = cls.get_or_insert(**init_data)
        activation.put()
        return activation

    @classmethod
    def get_pending(cls, user):
        """Get all pending activations for the given user."""
        query = cls.all()
        query.filter('email =', user.email())
        results = query.fetch(20)
        results = sorted(results, key=lambda i: i.key().parent().name())
        return results

    @classmethod
    def old_activations(cls, days=1):
        """Get old pending activations."""
        if days < 1:
            days = 1
        query = cls.all()
        yesterday = datetime.utcnow() - timedelta(days)
        query.filter('created <=', yesterday)
        query.order('created')
        return query.fetch(20)


from wowgae.model.guild import Guild
